<?php

/**
 * htmlform extension for datamapper classes.
 *
 * a powerful extension that allows one to quickly
 * generate standardized forms off a DMZ object.
 *
 * @license 	MIT License
 * @package		DMZ-Included-Extensions
 * @category	DMZ
 * @author  	Phil DeJarnett
 * @link    	http://www.overzealous.com/dmz/pages/extensions/htmlform.html
 * @version 	1.0
 */
// --------------------------------------------------------------------------

/**
 * DMZ_HTMLForm Class
 *
 * @package		DMZ-Included-Extensions
 */
class DMZ_HTMLForm {

    // this is the default template (view) to use for the overall form
    var $form_template = 'dmz_htmlform/form';
    // this is the default template (view) to use for the individual rows
    var $row_template = 'dmz_htmlform/row';
    // this is the default template (view) to use for the individual rows
    var $section_template = 'dmz_htmlform/section';
    var $auto_rule_classes = array(
        'integer' => 'integer',
        'numeric' => 'numeric',
        'is_natural' => 'natural',
        'is_natural_no_zero' => 'positive_int',
        'valid_email' => 'email',
        'valid_ip' => 'ip',
        'valid_base64' => 'base64',
        'valid_date' => 'date',
        'alpha_dash_dot' => 'alpha_dash_dot',
        'alpha_slash_dot' => 'alpha_slash_dot',
        'alpha' => 'alpha',
        'alpha_numeric' => 'alpha_numeric',
        'alpha_dash' => 'alpha_dash',
        'required' => 'required'
    );

    function __construct($options = array()) {
        foreach ($options as $k => $v) {
            $this->{$k} = $v;
        }
        $this->CI = & get_instance();
        $this->load = $this->CI->load;
    }

    // --------------------------------------------------------------------------

    /**
     * Render a single field.  Can be used to chain together multiple fields in a column.
     *
     * @param object $object The DataMapper Object to use.
     * @param string $field The field to render.
     * @param string $type  The type of field to render.
     * @param array  $options  Various options to modify the output.
     * @return Rendered String.
     */
    function render_field($object, $field, $type = NULL, $options = NULL) {
        $value = '';

        if (array_key_exists($field, $object->has_one) || array_key_exists($field, $object->has_many)) {
            // Create a relationship field
            $one = array_key_exists($field, $object->has_one);

            // attempt to look up the current value(s)
            if (!isset($options['value'])) {
                if ($this->CI->input->post($field)) {
                    $value = $this->CI->input->post($field);
                } else {
                    // load the related object(s)
                    $sel = $object->{$field}->select('id')->get();
                    if ($one) {
                        // only a single value is allowed
                        $value = $sel->id;
                    } else {
                        // save what might be multiple values
                        $value = array();
                        foreach ($sel as $s) {
                            $value[] = $s->id;
                        }
                    }
                }
            } else {
                // value was already set in the options
                $value = $options['value'];
                unset($options['value']);
            }

            // Attempt to get a list of possible values
            if (!isset($options['list']) || is_object($options['list'])) {
                if (!isset($options['list'])) {
                    // look up all of the related values
                    $c = get_class($object->{$field});
                    $total_items = new $c;
                    // See if the custom method is defined
                    if (method_exists($total_items, 'get_htmlform_list')) {
                        // Get customized list
                        $total_items->get_htmlform_list($object, $field);
                    } else {
                        // Get all items
                        $total_items->get_iterated();
                    }
                } else {
                    // process a passed-in DataMapper object
                    $total_items = $options['list'];
                }
                $list = array();
                foreach ($total_items as $item) {
                    // use the __toString value of the item for the label
                    $list[$item->id] = (string) $item;
                }
                $options['list'] = $list;
            }

            // By if there can be multiple items, use a dropdown for large lists,
            // and a set of checkboxes for a small one.
            if ($one || count($options['list']) > 6) {
                $default_type = 'dropdown';
                if (!$one && !isset($options['size'])) {
                    // limit to no more than 8 items high.
                    $options['size'] = min(count($options['list']), 8);
                }
            } else {
                $default_type = 'checkbox';
            }
        } else {
            // attempt to look up the current value(s)
            if (!isset($options['value'])) {
                if ($this->CI->input->post($field)) {
                    $value = $this->CI->input->post($field);
                    // clear default if set
                    unset($options['default_value']);
                } else {
                    if (isset($options['default_value'])) {
                        $value = $options['default_value'];
                        unset($options['default_value']);
                    } else {
                        // the field IS the value.
                        $value = $object->{$field};
                    }
                }
            } else {
                // value was already set in the options
                $value = $options['value'];
                unset($options['value']);
            }
            // default to text
            $default_type = ($field == 'id') ? 'hidden' : 'text';

            // determine default attributes
            $a = array();
            // such as the size of the field.
            $max = $this->_get_validation_rule($object, $field, 'max_length');
            if ($max === FALSE) {
                $max = $this->_get_validation_rule($object, $field, 'exact_length');
            }
            if ($max !== FALSE) {
                $a['maxlength'] = $max;
                $a['size'] = min($max, 30);
            }
            $list = $this->_get_validation_info($object, $field, 'values', FALSE);
            if ($list !== FALSE) {
                $a['list'] = $list;
            }
            $options = $options + $a;
            $extra_class = array();

            // Add any of the known rules as classes (for JS processing)
            foreach ($this->auto_rule_classes as $rule => $c) {
                if ($this->_get_validation_rule($object, $field, $rule) !== FALSE) {
                    $extra_class[] = $c;
                }
            }

            // add or set the class on the field.
            if (!empty($extra_class)) {
                $extra_class = implode(' ', $extra_class);
                if (isset($options['class'])) {
                    $options['class'] .= ' ' . $extra_class;
                } else {
                    $options['class'] = $extra_class;
                }
            }
        }

        // determine the renderer type
        $type = $this->_get_type($object, $field, $type);
        if (empty($type)) {
            $type = $default_type;
        }

        // attempt to find the renderer function
        if (method_exists($this, '_input_' . $type)) {
            return $this->{'_input_' . $type}($object, $field, $value, $options);
        } else if (function_exists('input_' . $type)) {
            return call_user_func('input_' . $type, $object, $field, $value, $options);
        } else {
            log_message('error', 'FormMaker: Unable to find a renderer for ' . $type);
            return '<span style="color: Maroon; background-color: White; font-weight: bold">FormMaker: UNABLE TO FIND A RENDERER FOR ' . $type . '</span>';
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Render a row with a single field.  If $field does not exist on
     * $object->validation, then $field is output as-is.
     *
     * @param object $object The DataMapper Object to use.
     * @param string $field The field to render (or content)
     * @param string $type  The type of field to render.
     * @param array  $options  Various options to modify the output.
     * @param string $row_template  The template to use, or NULL to use the default.
     * @return Rendered String.
     */
    function render_row($object, $field, $type = NULL, $options = array(), $row_template = NULL) {
        // try to determine type automatically
        $type = $this->_get_type($object, $field, $type);

        if (!isset($object->validation[$field]) && (empty($type) || $type == 'section' || $type == 'none')) {
            // this could be a multiple-field row, or just some text.
            // if $type is 'section, it will be rendered using the section template.
            $error = '';
            $label = '';
            $content = $field;
            $id = NULL;
        } else {
            // use validation information to render the field.
            $content = $this->render_field($object, $field, $type, $options);
            if (empty($row_template)) {
                if ($type == 'hidden' || $field == 'id') {
                    $row_template = 'none';
                } else {
                    $row_template = $this->row_template;
                }
            }
            // determine if there is an existing error
            $error = isset($object->error->{$field}) ? $object->error->{$field} : '';
            // determine if there is a pre-defined label
            $label = $this->_get_validation_info($object, $field, 'label', $field);
            // the field IS the id
            $id = $field;
        }

        $required = $this->_get_validation_rule($object, $field, 'required');

        // Append these items.  Values in $options have priority
        $view_data = $options + array(
            'object' => $object,
            'content' => $content,
            'field' => $field,
            'label' => $label,
            'error' => $error,
            'id' => $id,
            'required' => $required
        );

        if (is_null($row_template)) {
            if (empty($type)) {
                $row_template = 'none';
            } else if ($type == 'section') {
                $row_template = $this->section_template;
            } else {
                $row_template = $this->row_template;
            }
        }

        if ($row_template == 'none') {
            return $content;
        } else {
            return $this->load->view($row_template, $view_data, TRUE);
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Renders an entire form.
     *
     * @param object $object The DataMapper Object to use.
     * @param string $fields An associative array that defines the form.
     * @param string $template  The template to use.
     * @param string $row_template  The template to use for rows.
     * @param array  $template_options  The template to use for rows.
     * @return Rendered String.
     */
    function render_form($object, $fields, $url = '', $options = array(), $template = NULL, $row_template = NULL) {
        if (empty($url)) {
            // set url to current url
            $url = $this->CI->uri->uri_string();
        }

        if (is_null($template)) {
            $template = $this->form_template;
        }

        $rows = '';
        foreach ($fields as $field => $field_options) {
            $rows .= $this->_render_row_from_form($object, $field, $field_options, $row_template);
        }

        $view_data = $options + array(
            'object' => $object,
            'fields' => $fields,
            'url' => $url,
            'rows' => $rows
        );

        return $this->load->view($template, $view_data, TRUE);
    }

    // --------------------------------------------------------------------------
    // Private Methods
    // --------------------------------------------------------------------------
    // Converts information from render_form into a row of objects.
    function _render_row_from_form($object, $field, $options, $row_template, $row = TRUE) {
        if (is_int($field)) {
            // simple form, or HTML-content
            $field = $options;
            $options = NULL;
        }
        if (is_null($options)) {
            // always have an array for options
            $options = array();
        }

        $type = '';
        if (!is_array($options)) {
            // if options is a single string, assume it is the type.
            $type = $options;
            $options = array();
        }

        if (isset($options['type'])) {
            // type was set in options
            $type = $options['type'];
            unset($options['type']);
        }

        // see if a different row_template was in the options
        $rt = $row_template;
        if (isset($options['template'])) {
            $rt = $options['template'];
            unset($options['template']);
        }

        // Multiple fields, render them all as one.
        if (is_array($field)) {
            if (isset($field['row_options'])) {
                $options = $field['row_options'];
                unset($field['row_options']);
            }
            $ret = '';
            $sep = ' ';
            if (isset($field['input_separator'])) {
                $sep = $field['input_separator'];
                unset($field['input_separator']);
            }
            foreach ($field as $f => $fo) {
                // add each field to a list
                if (!empty($ret)) {
                    $ret .= $sep;
                }
                $ret .= $this->_render_row_from_form($object, $f, $fo, $row_template, FALSE);
            }

            // renders into a row or field below.
            $field = $ret;
        }
        if ($row) {
            // if row is set, render the whole row.
            return $this->render_row($object, $field, $type, $options, $rt);
        } else {
            // render just the field.
            return $this->render_field($object, $field, $type, $options);
        }
    }

    // --------------------------------------------------------------------------
    // Attempts to look up the field's type
    function _get_type($object, $field, $type) {
        if (empty($type)) {
            $type = $this->_get_validation_info($object, $field, 'type', NULL);
        }
        return $type;
    }

    // --------------------------------------------------------------------------
    // Returns a field from the validation array
    function _get_validation_info($object, $field, $val, $default = '') {
        if (isset($object->validation[$field][$val])) {
            return $object->validation[$field][$val];
        }
        return $default;
    }

    // --------------------------------------------------------------------------
    // Returns the value (or TRUE) of the validation rule, or FALSE if it does not exist.
    function _get_validation_rule($object, $field, $rule) {
        $r = $this->_get_validation_info($object, $field, 'rules', FALSE);
        if ($r !== FALSE) {
            if (isset($r[$rule])) {
                return $r[$rule];
            } else if (in_array($rule, $r, TRUE)) {
                return TRUE;
            }
        }
        return FALSE;
    }

    // --------------------------------------------------------------------------
    // Input Types
    // --------------------------------------------------------------------------
    // Render a hidden input
    function _input_hidden($object, $id, $value, $options) {
        return $this->_render_simple_input('hidden', $id, $value, $options);
    }

    // render a single-line text input
    function _input_text($object, $id, $value, $options) {
        return $this->_render_simple_input('text', $id, $value, $options);
    }

    // render a password input
    function _input_password($object, $id, $value, $options) {
        if (isset($options['send_value'])) {
            unset($options['send_value']);
        } else {
            $value = '';
        }
        return $this->_render_simple_input('password', $id, $value, $options);
    }

    // render a multiline text input
    function _input_textarea($object, $id, $value, $options) {
        if (isset($options['value'])) {
            $value = $options['value'];
            unset($options['value']);
        }
        $a = $options + array(
            'name' => $id,
            'id' => $id
        );
        return $this->_render_node('textarea', $a, htmlspecialchars($value));
    }

    // render a dropdown
    function _input_dropdown($object, $id, $value, $options) {
        $list = $options['list'];
        unset($options['list']);
        $selected = $value;
        if (isset($options['value'])) {
            $selected = $options['value'];
            unset($options['value']);
        }
        if (!is_array($selected)) {
            $selected = array($selected);
        } else {
            // force multiple
            $options['multiple'] = 'multiple';
        }
        $l = $this->_options($list, $selected);

        $name = $id;
        if (isset($options['multiple'])) {
            $name .= '[]';
        }
        $a = $options + array(
            'name' => $name,
            'id' => $id
        );
        return $this->_render_node('select', $a, $l);
    }

    // used to render an options list.
    function _options($list, $sel) {
        $l = '';
        foreach ($list as $opt => $label) {
            if (is_array($label)) {
                $l .= '<optgroup label="' . htmlspecialchars($key) . '">';
                $l .= $this->_options($label, $sel);
                $l .= '</optgroup>';
            } else {
                $a = array('value' => $opt);
                if (in_array($opt, $sel)) {
                    $a['selected'] = 'selected';
                }
                $l .= $this->_render_node('option', $a, htmlspecialchars($label));
            }
        }
        return $l;
    }

    // render a checkbox or series of checkboxes
    function _input_checkbox($object, $id, $value, $options) {
        return $this->_checkbox('checkbox', $id, $value, $options);
    }

    // render a series of radio buttons
    function _input_radio($object, $id, $value, $options) {
        return $this->_checkbox('radio', $id, $value, $options);
    }

    // renders one or more checkboxes or radio buttons
    function _checkbox($type, $id, $value, $options, $sub_id = '', $label = '') {
        if (isset($options['value'])) {
            $value = $options['value'];
            unset($options['value']);
        }
        // if there is a list in options, render our multiple checkboxes.
        if (isset($options['list'])) {
            $list = $options['list'];
            unset($options['list']);
            $ret = '';
            if (!is_array($value)) {
                if (is_null($value) || $value === FALSE || $value === '') {
                    $value = array();
                } else {
                    $value = array($value);
                }
            }
            $sep = '<br/>';
            if (isset($options['input_separator'])) {
                $sep = $options['input_separator'];
                unset($options['input_separator']);
            }
            foreach ($list as $k => $v) {
                if (!empty($ret)) {
                    // put each node on one line.
                    $ret .= $sep;
                }
                $ret .= $this->_checkbox($type, $id, $value, $options, $k, $v);
            }
            return $ret;
        } else {
            // just render the single checkbox.
            $node_id = $id;
            if (!empty($sub_id)) {
                // there are multiple nodes with this id, append the sub_id
                $node_id .= '_' . $sub_id;
                $field_value = $sub_id;
            } else {
                // sub_id is the same as the node's id
                $sub_id = $id;
                $field_value = '1';
            }
            $name = $id;
            if (is_array($value)) {
                // allow for multiple results
                $name .= '[]';
            }
            // node attributes
            $a = $options + array(
                'type' => $type,
                'id' => $node_id,
                'name' => $name,
                'value' => $field_value
            );
            // if checked wasn't overridden
            if (!isset($a['checked'])) {
                // determine if this is a multiple checkbox or not.
                $checked = $value;
                if (is_array($checked)) {
                    $checked = in_array($sub_id, $value);
                }
                if ($checked) {
                    $a['checked'] = 'checked';
                }
            }
            $ret = $this->_render_node('input', $a);
            if (!empty($label)) {
                $ret .= ' ' . $this->_render_node('label', array('for' => $node_id), $label);
            }
            return $ret;
        }
    }

    // render a file upload input
    function _input_file($object, $id, $value, $options) {
        $a = $options + array(
            'type' => 'file',
            'name' => $id,
            'id' => $id
        );
        return $this->_render_node('input', $a);
    }

    // Utility method to render a normal <input>
    function _render_simple_input($type, $id, $value, $options) {
        $a = $options + array(
            'type' => $type,
            'name' => $id,
            'id' => $id,
            'value' => $value
        );
        return $this->_render_node('input', $a);
    }

    // Utility method to render a node.
    function _render_node($type, $attributes, $content = FALSE) {
        // generate node
        $res = '<' . $type;
        foreach ($attributes as $att => $v) {
            // the special attribute '_' is rendered directly.
            if ($att == '_') {
                $res .= ' ' . $v;
            } else {
                if ($att != 'label') {
                    $res .= ' ' . $att . '="' . htmlspecialchars((string) $v) . '"';
                }
            }
        }
        // allow for content-containing nodes
        if ($content !== FALSE) {
            $res .= '>' . $content . '</' . $type . '>';
        } else {
            $res .= ' />';
        }
        return $res;
    }

}

/* End of file htmlform.php */
/* Location: ./application/datamapper/htmlform.php */